﻿// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

using Nova.CodeDOM;

namespace Nova.UI
{
    /// <summary>
    /// The view model for a <see cref="CodeDOM.Block"/>.
    /// </summary>
    public class BlockVM : CodeObjectVM, ICollection<CodeObjectVM>, ICollection
    {
        #region /* STATICS */

        internal static void AddViewModelMapping()
        {
            //// Force manual creation of BlockVMs, so that the parent VM can be properly set
            //CreateViewModel.Add(typeof(Block),
            //    delegate(CodeObject codeObject, bool isDescription, Dictionary<CodeObject, CodeObjectVM> dictionary) { return new BlockVM((Block)codeObject, null, dictionary); });
        }

        #endregion

        #region /* FIELDS */

        /// <summary>
        /// Child <see cref="CodeObjectVM"/>s - the Parent of this collection will be the BlockVM's Parent, so
        /// that the Parent of all child view models will be the BlockVM's Parent, not the BlockVM.
        /// </summary>
        protected ChildListVM<CodeObjectVM> _codeObjectVMs;

        /// <summary>
        /// Canvas used for virtualized display of child items.
        /// </summary>
        protected Canvas _canvas;

        #endregion

        #region /* CONSTRUCTORS */

        /// <summary>
        /// Create a view model instance for the specified <see cref="CodeDOM.Block"/>.
        /// </summary>
        public BlockVM(Block block, CodeObjectVM parentVM, Dictionary<CodeObject, CodeObjectVM> dictionary)
            : base(block, dictionary)
        {
            ParentVM = parentVM;
            _codeObjectVMs = parentVM.CreateListVM<CodeObject, CodeObjectVM>(block, dictionary);
        }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The underlying <see cref="CodeDOM.Block"/> model.
        /// </summary>
        public Block Block
        {
            get { return (Block)CodeObject; }
        }

        /// <summary>
        /// The number of code objects in the <see cref="BlockVM"/>.
        /// </summary>
        public int Count
        {
            get { return (_codeObjectVMs != null ? _codeObjectVMs.Count : 0); }
        }

        /// <summary>
        /// Always <c>false</c>.
        /// </summary>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <summary>
        /// True if access to the <see cref="ICollection"/> is synchronized.
        /// </summary>
        public virtual bool IsSynchronized
        {
            get { return false; }
        }

        /// <summary>
        /// Gets an object that can be used to synchronize access to the <see cref="ICollection"/>.
        /// </summary>
        public virtual object SyncRoot
        {
            get { return this; }
        }

        /// <summary>
        /// Get the child <see cref="CodeObjectVM"/> at the specified index.
        /// </summary>
        public CodeObjectVM this[int index]
        {
            get { return _codeObjectVMs[index]; }
        }

        /// <summary>
        /// Get the last <see cref="CodeObjectVM"/> in the <see cref="BlockVM"/>.
        /// </summary>
        public CodeObjectVM Last
        {
            get { return _codeObjectVMs.Last; }
        }

        /// <summary>
        /// True if the <see cref="BlockVM"/> is rendered at the top level (has no indent).
        /// </summary>
        public bool IsTopLevel
        {
            get
            {
                CodeObject parent = ParentVM.CodeObject;
                return (!(parent is IBlock) || ((IBlock)parent).IsTopLevel);
            }
        }

        #endregion

        #region /* METHODS */

        /// <summary>
        /// Add a code view model to the <see cref="BlockVM"/>.
        /// </summary>
        /// <param name="obj">The object to be added.</param>
        public void Add(CodeObjectVM obj)
        {
            _codeObjectVMs.Add(obj);
        }

        /// <summary>
        /// Clear all members from the <see cref="BlockVM"/>.
        /// </summary>
        public void Clear()
        {
            RemoveAll();
        }

        /// <summary>
        /// Check if the block contains the specified code view model.
        /// </summary>
        /// <param name="codeObject">The object being searched for.</param>
        /// <returns>True if the block contains the object, otherwise false.</returns>
        public bool Contains(CodeObjectVM codeObject)
        {
            return Enumerable.Any(this, delegate(CodeObjectVM child) { return child == codeObject; });
        }

        /// <summary>
        /// Copy the code view models in the block to the specified array, starting at the specified offset.
        /// </summary>
        /// <param name="codeObjects">The array to copy into.</param>
        /// <param name="index">The starting index in the array.</param>
        public void CopyTo(CodeObjectVM[] codeObjects, int index)
        {
            CopyTo((Array)codeObjects, index);
        }

        /// <summary>
        /// Copy the code view models in the block to the specified array, starting at the specified offset.
        /// </summary>
        /// <param name="array">The array to copy into.</param>
        /// <param name="index">The starting index in the array.</param>
        public void CopyTo(Array array, int index)
        {
            if (array == null)
                throw new ArgumentNullException("array", "Null array reference");
            if (index < 0)
                throw new ArgumentOutOfRangeException("index", "Index is out of range");
            if (array.Rank > 1)
                throw new ArgumentException("Array is multi-dimensional", "array");

            foreach (CodeObjectVM obj in this)
                array.SetValue(obj, index++);
        }

        /// <summary>
        /// Insert a code view model into the block at the specified index.
        /// </summary>
        /// <param name="index">The index at which to insert.</param>
        /// <param name="codeObject">The object to be inserted.</param>
        public void Insert(int index, CodeObjectVM codeObject)
        {
            // Insert the code view model into the block
            _codeObjectVMs.Insert(index, codeObject);
        }

        /// <summary>
        /// Find the first child code view model having type T.
        /// </summary>
        public T Find<T>() where T : CodeObjectVM
        {
            return Enumerable.FirstOrDefault(Enumerable.OfType<T>(_codeObjectVMs));
        }

        /// <summary>
        /// Get an enumerator for the code view models in the <see cref="BlockVM"/>.
        /// </summary>
        IEnumerator<CodeObjectVM> IEnumerable<CodeObjectVM>.GetEnumerator()
        {
            return ((IEnumerable<CodeObjectVM>)_codeObjectVMs).GetEnumerator();
        }

        /// <summary>
        /// Get an enumerator for the code view models in the <see cref="BlockVM"/>.
        /// </summary>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return _codeObjectVMs.GetEnumerator();
        }

        /// <summary>
        /// Remove the specified code view model from the <see cref="BlockVM"/>.
        /// </summary>
        /// <returns>True if the code object was found and removed, otherwise false.</returns>
        public bool Remove(CodeObjectVM codeObject)
        {
            return _codeObjectVMs.Remove(codeObject);
        }

        /// <summary>
        /// Remove the code view model at the specified index from the <see cref="BlockVM"/>.
        /// </summary>
        public void RemoveAt(int index)
        {
            _codeObjectVMs.RemoveAt(index);
        }

        /// <summary>
        /// Remove all code view models from the <see cref="BlockVM"/>.
        /// </summary>
        public void RemoveAll()
        {
            _codeObjectVMs.Clear();
        }

        #endregion

        #region /* RENDERING */

        public override Brush BorderBrush
        {
            get { return (ParentVM != null ? ParentVM.BorderBrush : base.BorderBrush); }
        }

        public override Brush BackgroundBrush
        {
            get { return (ParentVM != null ? ParentVM.BackgroundBrush : base.BackgroundBrush); }
        }

        /// <summary>
        /// Determines if braces should be hidden.
        /// </summary>
        public static bool HideBraces;

        /// <summary>
        /// Determines if braces should be tiny.
        /// </summary>
        public static bool TinyBraces;

        public override void Render(CodeRenderer renderer, RenderFlags flags)
        {
            Block block = Block;
            if (block.IsGenerated)
                return;
            if (flags.HasFlag(RenderFlags.Description))
            {
                TypeRefBaseVM.RenderType(renderer, Block.GetType(), RenderFlags.None, this);
                return;
            }

            bool isTopLevel = IsTopLevel;
            if (isTopLevel)
                flags |= RenderFlags.NoBlockIndent;

            // Render the open brace if appropriate
            bool useBraces = block.HasBraces && !isTopLevel && !HideBraces;
            if (useBraces || block.HasFirstOnLineAnnotations)
            {
                if (IsFirstOnLine)
                {
                    if (!flags.HasFlag(RenderFlags.SuppressNewLine))
                        renderer.NewLine();
                }
                else
                    renderer.RenderText(" ", PUNC_BRUSH, ParentVM);

                RenderBefore(renderer, flags);
                if (useBraces)
                {
                    renderer.RenderText(Block.ParseTokenStart, PUNC_BRUSH, CodeRenderer.CodeFontFamily,
                        TinyBraces ? CodeRenderer.TinyFontSize : CodeRenderer.CodeFontSize, 0, TextStyle.Normal, ParentVM);
                }
                flags &= ~RenderFlags.SuppressNewLine;
            }

            if (!flags.HasFlag(RenderFlags.NoEOLComments))
                RenderInfixEOLComments(renderer, flags);

            if (!flags.HasFlag(RenderFlags.NoBlockIndent))
                renderer.IndentOnNewLineBegin(this, CodeObject.TabSize - 1);  // Allow for SpaceIndent

            // Render the body of the block
            bool isSingleLineBody = true;
            int codeObjectVMCount = (_codeObjectVMs != null ? _codeObjectVMs.Count : 0);

            // Render an empty statement ';' if we have no children or a single comment and no braces
            CodeObject parent = ParentVM.CodeObject;
            if ((codeObjectVMCount == 0 || (codeObjectVMCount == 1 && _codeObjectVMs[0] is CommentVM)) && !block.HasBraces && (!(parent is BlockStatement) || ((BlockStatement)parent).RequiresEmptyStatement))
            {
                if (IsFirstOnLine)
                {
                    if (!flags.HasFlag(RenderFlags.SuppressNewLine))
                        renderer.NewLine();
                }
                else
                    renderer.RenderText(" ", PUNC_BRUSH, ParentVM);
                renderer.RenderText(Statement.ParseTokenTerminator, PUNC_BRUSH, ParentVM);
                if (codeObjectVMCount > 0 && !CommentBaseVM.HideAll)
                    renderer.RenderText(" ", PUNC_BRUSH, ParentVM);
                flags |= RenderFlags.SuppressNewLine;
            }

            if (codeObjectVMCount > 0)
            {
                RenderFlags passFlags = (flags & (RenderFlags.PassMask | RenderFlags.SuppressNewLine)) | RenderFlags.PrefixSpace;
                bool firstItem = true;
                bool skippingComment = false;
                int skippedCommentNewLines = 0;
                bool measureBlock = false, hasCanvas = false;
                double lineWidth = 0, lineHeight = 0;
                if (renderer.Measuring)
                    measureBlock = true;
                else
                {
                    hasCanvas = (_codeObjectVMs.Count > 1 && Height > 0 && Y > 0 && CodeRenderer.VirtualizeRendering);
                    if (hasCanvas)
                    {
                        renderer.NewLine();
                        _canvas = renderer.CreateCanvas(Height, Width);
                    }
                }

                // Process the child members of the block
                for (int i = 0; i < codeObjectVMCount; ++i)
                {
                    CodeObjectVM codeObjectVM = _codeObjectVMs[i];
                    CodeObject codeObject = codeObjectVM.CodeObject;
                    if (codeObject.IsGenerated)
                        continue;

                    int objNewLines = codeObject.NewLines;
                    if (CommentBaseVM.HideAll && codeObjectVM is CommentBaseVM)
                    {
                        // Skip comments if option is enabled
                        skippingComment = true;
                        skippedCommentNewLines = Math.Max(skippedCommentNewLines, objNewLines);
                    }
                    else
                    {
                        // Check for newlines, temporarily adjusting as necessary if we're skipping hidden comment(s)
                        int backupObjNewLines = objNewLines;
                        if (skippingComment)
                        {
                            if (firstItem)
                                objNewLines = 1;
                            else if (objNewLines <= 1 && skippedCommentNewLines > 1)
                                objNewLines = skippedCommentNewLines;
                            codeObject.SetNewLines(objNewLines);
                        }
                        if (objNewLines > 0)
                            isSingleLineBody = false;

                        // Do calculations if we're measuring (so we can do partial rendering with a canvas)
                        if (measureBlock)
                        {
                            if (codeObjectVM.IsFirstOnLine)
                            {
                                // For the first item, get the current absolute Y of the Canvas and store it in the Y of the BlockVM
                                // (if Y isn't non-zero for a BlockVM, it won't be virtualized with a Canvas).
                                // Include any newline that hasn't been rendered yet (after the closing brace).
                                if (firstItem && ParentVM is IBlockVM)
                                    Y = renderer.GetCurrentAbsoluteY() + (flags.HasFlag(RenderFlags.SuppressNewLine) ? 0 : CodeRenderer.FontHeight);

                                if (objNewLines > 1)
                                    Height += (objNewLines - 1) * (CodeRenderer.HalfHeightBlankLines ? (CodeRenderer.FontHeight / 2) : CodeRenderer.FontHeight);
                                codeObjectVM.Y = Height;
                            }
                            else
                            {
                                // Don't measure the block if the first item isn't first-on-line (partial rendering won't be done)
                                if (firstItem)
                                    measureBlock = false;
                                else
                                {
                                    // Handle 2nd or Nth object on the same line
                                    CodeObjectVM previousVM = _codeObjectVMs[i - 1];
                                    codeObjectVM.Y = previousVM.Y;
                                    lineWidth += CodeRenderer.SpaceWidth;
                                    codeObjectVM.X = lineWidth;
                                }
                            }
                        }

                        // If we're using a canvas, skip rendering if the code object occurs before OR after the visible window
                        if (!hasCanvas || (Y + codeObjectVM.Y + codeObjectVM.Height >= renderer.StartY && Y + codeObjectVM.Y < renderer.EndY))
                        {
                            // Render the code object (do NOT increase the indent, the indent state logic will take care of that)
                            codeObjectVM.Render(renderer, passFlags | RenderFlags.ForceBorder | RenderFlags.NoIncreaseIndent
                                | ((isSingleLineBody || flags.HasFlag(RenderFlags.NoBlockIndent)) ? 0
                                : (objNewLines > 0 ? RenderFlags.SpaceIndent : 0)));
                        }

                        // Do calculations if we're measuring (so we can do partial rendering with a canvas)
                        if (measureBlock)
                        {
                            if (codeObjectVM.IsFirstOnLine)
                            {
                                Height += codeObjectVM.Height;
                                lineWidth = codeObjectVM.Width;
                            }
                            else
                            {
                                // Handle 2nd or Nth object on the same line
                                CodeObjectVM previousVM = _codeObjectVMs[i - 1];
                                if (lineHeight == 0)
                                    lineHeight = previousVM.Height;
                                if (codeObjectVM.Height > lineHeight)
                                {
                                    Height += (codeObjectVM.Height - lineHeight);
                                    lineHeight = codeObjectVM.Height;
                                }
                                lineWidth += codeObjectVM.Width;
                                if (i >= codeObjectVMCount - 1 || _codeObjectVMs[i + 1].IsFirstOnLine)
                                {
                                    // Adjust the Y positions of all objects on the line to center them
                                    for (int j = i; ; --j)
                                    {
                                        CodeObjectVM lineVM = _codeObjectVMs[j];
                                        lineVM.Y += (lineHeight - lineVM.Height) / 2;
                                        if (lineVM.IsFirstOnLine)
                                            break;
                                    }
                                    lineHeight = 0;
                                }
                            }
                            if (lineWidth > Width)
                                Width = lineWidth;
                        }

                        // Undo any temporary changes to the newlines
                        if (skippingComment)
                            codeObject.SetNewLines(backupObjNewLines);

                        firstItem = false;
                        skippingComment = false;
                        skippedCommentNewLines = 0;
                    }

                    passFlags &= ~RenderFlags.SuppressNewLine;
                    flags &= ~RenderFlags.SuppressNewLine;
                }

                if (hasCanvas)
                    renderer.ReturnToCanvasParent();
            }

            if (!flags.HasFlag(RenderFlags.NoBlockIndent))
                renderer.IndentOnNewLineEnd(this);

            // Render the close brace if appropriate
            if (useBraces)
            {
                int endNewLines = block.EndNewLines;
                if (endNewLines > 0)
                    renderer.NewLines(endNewLines, ParentVM);
                else
                    renderer.RenderText(" ", PUNC_BRUSH, ParentVM);

                renderer.RenderText(Block.ParseTokenEnd, PUNC_BRUSH, CodeRenderer.CodeFontFamily,
                    TinyBraces ? CodeRenderer.TinyFontSize : CodeRenderer.CodeFontSize, 0, TextStyle.Normal, ParentVM);
            }

            // Render any EOL comments (rendered after the close brace when it's on a line by itself - special Infix
            // EOL comments may also exist and be rendered after the open brace above).
            if (!flags.HasFlag(RenderFlags.NoEOLComments))
                RenderEOLComments(renderer, RenderFlags.None);

            RenderAfter(renderer, flags);
        }

        public override void RenderVisible(CodeRenderer renderer, RenderFlags flags)
        {
            Block block = Block;
            if (block.IsGenerated)
                return;

            if (IsTopLevel)
                flags |= RenderFlags.NoBlockIndent;

            // Render the body of the block
            if (_codeObjectVMs != null && _codeObjectVMs.Count > 0)
            {
                RenderFlags passFlags = (flags & RenderFlags.PassMask) | RenderFlags.PrefixSpace;
                bool firstItem = true;
                bool skippingComment = false;
                int skippedCommentNewLines = 0;
                bool hasCanvas = (_canvas != null);

                // Process the child members of the block
                for (int i = 0; i < _codeObjectVMs.Count; ++i)
                {
                    CodeObjectVM codeObjectVM = _codeObjectVMs[i];
                    CodeObject codeObject = codeObjectVM.CodeObject;
                    if (codeObject.IsGenerated)
                        continue;

                    int objNewLines = codeObject.NewLines;
                    if (CommentBaseVM.HideAll && codeObjectVM is CommentBaseVM)
                    {
                        // Skip comments if option is enabled
                        skippingComment = true;
                        skippedCommentNewLines = Math.Max(skippedCommentNewLines, objNewLines);
                    }
                    else
                    {
                        // Check for newlines, temporarily adjusting as necessary if we're skipping hidden comment(s)
                        int backupObjNewLines = objNewLines;
                        if (skippingComment)
                        {
                            if (firstItem)
                                objNewLines = 1;
                            else if (objNewLines <= 1 && skippedCommentNewLines > 1)
                                objNewLines = skippedCommentNewLines;
                            codeObject.SetNewLines(objNewLines);
                        }

                        // If we're using a canvas, render or free as appropriate, otherwise recursively call RenderVisible()
                        if (hasCanvas)
                        {
                            // If the object is visible, render it now if it's not rendered
                            if (Y + codeObjectVM.Y + codeObjectVM.Height >= renderer.StartY && Y + codeObjectVM.Y < renderer.EndY)
                            {
                                if (codeObjectVM.FrameworkElement == null)
                                {
                                    // Render the code object (do NOT increase the indent, the indent state logic will take care of that)
                                    renderer.SetCurrentCanvas(_canvas);
                                    codeObjectVM.Render(renderer, passFlags | RenderFlags.ForceBorder | RenderFlags.NoIncreaseIndent
                                        | (flags.HasFlag(RenderFlags.NoBlockIndent) ? 0 : (objNewLines > 0 ? RenderFlags.SpaceIndent : 0)));
                                }
                                else
                                    codeObjectVM.RenderVisible(renderer, passFlags);
                            }
                            else if (codeObjectVM.FrameworkElement != null)
                            {
                                // Recursively free all UI objects if it's no longer visible
                                _canvas.Children.Remove(codeObjectVM.FrameworkElement);
                                codeObjectVM.UnRender();
                            }
                        }
                        else
                            codeObjectVM.RenderVisible(renderer, passFlags);

                        // Undo any temporary changes to the newlines
                        if (skippingComment)
                            codeObject.SetNewLines(backupObjNewLines);

                        firstItem = false;
                        skippingComment = false;
                        skippedCommentNewLines = 0;
                    }
                }

                if (hasCanvas)
                    renderer.ReturnToCanvasParent();
            }
        }

        public override void UnRender()
        {
            ChildListHelpers.UnRender(_codeObjectVMs);
            base.UnRender();
        }

        public override void RenderToolTip(CodeRenderer renderer, RenderFlags flags)
        {
            TypeRefBaseVM.RenderType(renderer, CodeObject.GetType(), flags, this);
            RenderMessagesInToolTip(renderer, flags);
        }

        #endregion

        #region /* COMMANDS */

        public static readonly RoutedCommand TinyBracesCommand = new RoutedCommand("Tiny Braces", typeof(BlockVM));
        public static readonly RoutedCommand HideBracesCommand = new RoutedCommand("Hide Braces", typeof(BlockVM));

        static BlockVM()
        {
            InitializeCommands();
        }

        private static void InitializeCommands()
        {
            HideBracesCommand.InputGestures.Add(new KeyGesture(Key.OemCloseBrackets, ModifierKeys.Alt, "Alt-]"));
        }

        public static new void BindCommands(Window window)
        {
            WPFUtil.AddCommandBinding(window, TinyBracesCommand, tinyBraces_Executed);
            WPFUtil.AddCommandBinding(window, HideBracesCommand, hideBraces_Executed);
        }

        private static void tinyBraces_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            TinyBraces = !TinyBraces;
            CodeRenderer.ReRenderAll();
        }

        private static void hideBraces_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            HideBraces = !HideBraces;
            CodeRenderer.ReRenderAll();
        }

        #endregion
    }
}
